package cn.rocksolid.sport.common.utils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

public class ClazzUtils {

  private static final String PKG_SEPARATOR = ".";
  private static final String FILE_SEPARATOR = System.getProperty("file.separator");
  private static final String FILE_PROTOCOL = "file";
  private static final String JARBALL_PROTOCOL = "jar";
  private static final String JARBALL_PATH_PREFIX = "file:";
  private static final String JARBALL_SUFFIX_SEPARATOR = "!";
  private static final String EXT_OF_CLASSFILE = PKG_SEPARATOR + "class";

  private static final Logger LOGGER = Logger.getLogger(ClazzUtils.class.getName());

  private ClazzUtils() {
  }

  public static Set<String> lookupClassNamesByPackageName(String packageName, boolean isShallow,
      Pattern fileNamePattern) {
    Set<Class<?>> classes = lookupClassesByPackageName(packageName, isShallow, fileNamePattern, true, false, false);
    if (CollectionUtils.isEmpty(classes)) {
      return Collections.emptySet();
    }
    return classes.stream().map(Class::getName).collect(Collectors.toSet());
  }

  public static Set<String> lookupInterfaceNamesByPackageName(String packageName, boolean isShallow,
      Pattern fileNamePattern) {
    Set<Class<?>> classes = lookupClassesByPackageName(packageName, isShallow, fileNamePattern, false, true, false);
    if (CollectionUtils.isEmpty(classes)) {
      return Collections.emptySet();
    }
    return classes.stream().map(Class::getName).collect(Collectors.toSet());
  }

  public static Set<Class<?>> lookupClassesByPackageName(String packageName, boolean isShallow,
      Pattern fileNamePattern, boolean includeClass, boolean includeInterface, boolean includeEnum) {

    if (StringUtils.isBlank(packageName)) {
      return Collections.emptySet();
    }

    Set<Class<?>> classes = new HashSet<>();
    String packageResName = packageName.replace(PKG_SEPARATOR, FILE_SEPARATOR);
    try {
      Enumeration<URL> detectedUrls = Thread.currentThread().getContextClassLoader().getResources(packageResName);
      while (detectedUrls.hasMoreElements()) {
        URL url = detectedUrls.nextElement();
        if (FILE_PROTOCOL.equals(url.getProtocol())) {
          classes.addAll(lookupClassesFromFs(url.getFile(), packageName, isShallow, fileNamePattern, includeClass,
              includeInterface, includeEnum));
        } else if (JARBALL_PROTOCOL.equals(url.getProtocol())) {
          String pureJarLocation = StringUtils
              .substringBefore(StringUtils.removeStart(url.getFile(), JARBALL_PATH_PREFIX), JARBALL_SUFFIX_SEPARATOR);
          classes.addAll(lookupClassesFromJarball(pureJarLocation, packageName, isShallow, fileNamePattern,
              includeClass, includeInterface, includeEnum));
        } else {
          LOGGER.info("Unsupported protocol found when looking up interfaces in given package.");
        }
      }

    } catch (IOException ioe) {
      LOGGER.warning("Lookup classes error, caused by: " + ioe.getMessage());
    }

    return classes;
  }

  private static Set<Class<?>> lookupClassesFromFs(String fileLocation, String packageName, boolean isShallow,
      Pattern fileNamePattern, boolean includeClass, boolean includeInterface, boolean includeEnum) {

    File targetDirectory = new File(fileLocation);
    if (!targetDirectory.exists() || !targetDirectory.isDirectory()) {
      return Collections.emptySet();
    }

    File[] allOfFilesFound = targetDirectory.listFiles(f -> (f.isDirectory() && !isShallow)
        || (!f.isDirectory() && ((null == fileNamePattern || fileNamePattern.matcher(f.getName()).matches())
            && f.getName().endsWith(EXT_OF_CLASSFILE))));

    if (ArrayUtils.isEmpty(allOfFilesFound)) {
      return Collections.emptySet();
    }

    Set<Class<?>> classes = new HashSet<>();
    for (File targetFile : allOfFilesFound) {
      if (targetFile.isDirectory()) {
        String subPackageName = packageName + PKG_SEPARATOR + targetFile.getName();
        classes.addAll(lookupClassesFromFs(targetFile.getAbsolutePath(), subPackageName, isShallow, fileNamePattern,
            includeClass, includeInterface, includeEnum));
      } else {
        String classFullName = packageName + PKG_SEPARATOR
            + StringUtils.removeEnd(targetFile.getName(), EXT_OF_CLASSFILE);
        determinateClass(classes, classFullName, includeClass, includeInterface, includeEnum);
      }
    }
    return classes;
  }

  private static Set<Class<?>> lookupClassesFromJarball(String jarballLocation, String packageName,
      boolean isShallow, Pattern fileNamePattern, boolean includeClass, boolean includeInterface, boolean includeEnum) {

    String packagePathPrefix = packageName.replace(PKG_SEPARATOR, FILE_SEPARATOR);

    try (JarFile jarball = new JarFile(jarballLocation)) {
      Set<Class<?>> classes = new HashSet<>();
      Enumeration<JarEntry> ej = jarball.entries();
      while (ej.hasMoreElements()) {
        JarEntry je = ej.nextElement();
        if (StringUtils.startsWith(je.getName(), packagePathPrefix)
            && (!isShallow || !StringUtils
                .contains(StringUtils.substringAfter(je.getName(), packagePathPrefix + FILE_SEPARATOR), FILE_SEPARATOR))
            && (null == fileNamePattern || fileNamePattern.matcher(je.getName()).matches())
            && je.getName().endsWith(EXT_OF_CLASSFILE)) {
          String classFullName = StringUtils.removeEnd(je.getName().replace(FILE_SEPARATOR, PKG_SEPARATOR),
              EXT_OF_CLASSFILE);
          determinateClass(classes, classFullName, includeClass, includeInterface, includeEnum);
        }
      }
      return classes;
    } catch (IOException ioe) {
      LOGGER.warning("Can not open jarball, caused by: " + ioe.getMessage());
      return Collections.emptySet();
    }
  }

  private static void determinateClass(Set<Class<?>> classes, String classFullName, boolean includeClass,
      boolean includeInterface, boolean includeEnum) {
    try {
      Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(classFullName);
      if ((includeClass && !clazz.isInterface() && !clazz.isEnum()) || (clazz.isInterface() && includeInterface)
          || (clazz.isEnum() && includeEnum)) {
        classes.add(clazz);
      }
    } catch (ClassNotFoundException cnfe) {
      LOGGER.warning(String.format("Target class %s is not found, caused by: %s", classFullName, cnfe.getMessage()));
    }
  }
}
